package com.louzhu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.louzhu.gulimall.product.service.CategoryBrandRelationService;
import com.louzhu.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.louzhu.common.utils.PageUtils;
import com.louzhu.common.utils.Query;

import com.louzhu.gulimall.product.dao.CategoryDao;
import com.louzhu.gulimall.product.entity.CategoryEntity;
import com.louzhu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 逻辑删除
     * @param asList
     */
    @Override
    public void logicRemoveByIds(List<Long> asList) {
        // TODO 1. 检查当前要删除的分类是否被引用,没有被引用才能执行删除
        // 使用逻辑删除：使用某一个字段作为标志位 比如 pms_category 表 中使用 show_status 字段 1表示未删除0表示删除
        baseMapper.deleteBatchIds(asList);

    }

    /**
     * 获取三级分类树形结构数据
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        //查出分类表所有数据
        List<CategoryEntity> entityList = baseMapper.selectList(null);
        //查询出所有的一级分类数据.  使用stream的api
        List<CategoryEntity> firstEntityList = entityList.stream().
                filter(categoryEntity -> categoryEntity.getParentCid().equals(0L)).map((menu) -> {
            //传递当前的商品分类, 和所有的分类,递归查询出子分类
            menu.setChildren( getChildrens(menu,entityList));
            return menu;
        } ).sorted((menu1,menu2)->{
            //进行排序
            return (menu1.getSort() ==null?0:menu1.getSort())- (menu2.getSort()==null?0 : menu2.getSort());
        }).collect(Collectors.toList());
        return firstEntityList;
    }

    /**
     * 根据分类ID查询
     * @param catelogId
     * @return
     */
    @Override
    public Long[] finCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        paths = findParentPath(catelogId,paths);

        // 集合逆序
        Collections.reverse(paths);
        return paths.toArray(new Long[paths.size()]);
    }

    /**
     * 修改
     * @param category
     */

    // 批量清除category下的数据
    @CacheEvict(value = {"category"},allEntries = true)
    // 逐一删除
//    @Caching(evict = {
//            // 删除category缓存下key为getFirstLevelCatelog的数据
//            @CacheEvict(value = "category",key = "'getFirstLevelCatelog'"),
//            // 删除category缓存下key为getCatalogJson的数据
//            @CacheEvict(value = "category",key = "'getCatalogJson'")
//    })
    @Transactional
    @Override
    public void updateDetail(CategoryEntity category) {
        this.updateById(category);
        /*
        同步冗余字段值
         */
        if(!StringUtils.isEmpty(category.getName())){
            // 同步更新品牌-分配关联表中的分类名称
            categoryBrandRelationService.updateCatelogNameByCatelogId(category.getCatId(),category.getName());
        }
    }

    /**
     * 查询所有一级分类
     * @return
     */
    @Override
    /*
     使用缓存，调用到这个方法时，先从缓存查询数据，缓存不存在才从数据库查询，
     如果存在则从缓存获取不执行方法
     category为缓存分区名称（一般按业务分区）
     该注解的默认定义：
        如果缓存中存在数据，则不调用方法
        key默认自动生成，格式： 缓存分区名称::SimpleKey [] , 例如 category::SimpleKey []
        value值默认使用jdk序列化机制进行序列化存储到redis
        默认过期时间 TTL = -1 表示永不过期
    可以根据实际需求自定义：
        指定缓存数据的key值 -- spel表达式
            key = "'levelCatelog'" 表示以 levelCatelog 为键值
            key = "#root.method.name" 表示用方法名作为键值
        指定缓存数据的TTL值
        指定缓存数据以JSON格式序列化存储
     */
    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    public List<CategoryEntity> getFirstLevelCatelog() {
        System.out.println("执行查询数据库。。。");
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
    }

    /**
     * 在指定的分类数据集合中，查找指定分类的下级分类数据集合
     * @param categoryEntityList
     * @param parentCid
     * @return
     */
    public List<CategoryEntity> getSunCatelog(List<CategoryEntity> categoryEntityList,Long parentCid){
        List<CategoryEntity> collect = categoryEntityList.stream().filter(item -> {
            return item.getParentCid().equals(parentCid);
        }).collect(Collectors.toList());
        return collect;
    }

    @Cacheable(value = "category",key = "#root.methodName",sync = true)
    @Override
    public  Map<String, List<Catelog2Vo>> getCatalogJson(){
        // 查询出所有分类
        List<CategoryEntity> categoryEntityList = this.list();
        /*
        1 查出所有一级分类
         */
        List<CategoryEntity> categoryEntities1 = getSunCatelog(categoryEntityList,0L);
        /*
        2 封装一级分类下的2/3级分类
         */
        // // map的key:一级分类ID
        Map<String, List<Catelog2Vo>> result = categoryEntities1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // map的value：二级分类数据
            List<CategoryEntity> categoryEntities2 = getSunCatelog(categoryEntityList,v.getCatId());
            List<Catelog2Vo> catelog2Vos = null;
            if (null != categoryEntities2) {
                catelog2Vos = categoryEntities2.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getCatId().toString(), l2.getName(), null);
                    // 查找当前二级分类的三级分类
                    List<CategoryEntity> categoryEntities3 = getSunCatelog(categoryEntityList,l2.getCatId());
                    // 封装为页面需要的VO模型
                    if(null != categoryEntities3){
                        List<Catelog2Vo.Catelog3Vo> catelog3Vos = categoryEntities3.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catelog3Vos);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return result;

    }

    // TODO 排查堆外内存溢出问题
    public  Map<String, List<Catelog2Vo>> getCatalogJson2(){

        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 先从缓存获取
        String catalogJson = ops.get("catalogJson");
        // 如果缓存中不存在
        if(StringUtils.isEmpty(catalogJson)){
            System.out.println("**********缓存没有，准备获取锁*****************");
            // 从数据库查询数据(本地锁方式)
            // Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbwithLocalLock();
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbwithRedisLock();

            return catalogJsonFromDb;
        }
        System.out.println("**********缓存中有，直接返回*****************");
        // 直接将缓存中获取到的数据转为对应的类型返回
        return JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});

    }

    /**
     * Redisson 分布式锁 方式从数据库查询数据
     */
    public  Map<String, List<Catelog2Vo>> getCatalogJsonFromDbwithRedissonLock() {

        // 获取分布式锁
        RLock catalogJson_lock = redisson.getLock("catalogJson_lock");
        // 加锁
        catalogJson_lock.lock();
        // 执行查询数据库业务代码
        Map<String, List<Catelog2Vo>> catalogDataFromDb ;
        try{
            catalogDataFromDb = getCatalogDataFromDb();
        }finally {
            // 释放锁
            catalogJson_lock.unlock();
        }
        return catalogDataFromDb;


    }

    /**
     * 分布式锁方式从数据库查询数据
     */
    public  Map<String, List<Catelog2Vo>> getCatalogJsonFromDbwithRedisLock() {
        /*
        分布式占锁，去redis占坑
            使用 SET key value [EX seconds] [PX milliseconds] [NX|XX] 方式
            向Redis 存入一个 键为Lock 值为 1 set类型为NX(不存在才能成功set)  的数据作为锁
            成功set的线程表示占有锁，其他线程没有成功set就需要等待锁（自旋等待锁）
            释放锁：持有锁的线程执行完业务后删除redis中的 lokc 表示释放锁
            避免死锁：为避免在执行释放锁之前出现异常导致无法释放锁最终死锁的问题，需要设置自动过期时间
            为避免占锁成功、设置过期时间失败，设置锁过期时间与占锁操作必须是一个原子性操作
            假如某个线程占锁之后业务代码还没有执行完成(超时)，在redis中的锁 lock 先自动过期，这时其他线程成功占锁（设置了新的lock）,
                当前线程执行结束后直接删除锁会删除其他线程的lock，所以加锁的时候需要指定uuid 每个线程标识自己的lock
            假如删除锁操作时，从redis获取到lock是自己的锁，正准备删除时，锁自动过期其他线程占锁成功，也有可能造成删除其他线程的锁
                所以获取锁判断、与删锁操作也需要是一个原子操作来避免这个问题
         */
        // 使用UUID作为lock标识，区分各自的lock
        String uuid = UUID.randomUUID().toString();
        Boolean ok = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if(ok){
            // 占锁成功 查询数据库获取数据
            System.out.println("---------------------占锁成功 即将查询数据库----------------");
            Map<String, List<Catelog2Vo>> catalogDataFromDb ;
            try{
                catalogDataFromDb = getCatalogDataFromDb();
            }finally {
                // 释放锁（删除自己的lock）
            /*
            这种方式：获取判断与删除不是原子操作
            String lock = redisTemplate.opsForValue().get("lock");
            if(lock.equals(uuid)){
                // 当前锁是自己的才能删除
                redisTemplate.delete("lock");
            }
            */
            /*
            lua脚本 实现 get和delete原子操作
             */
                String lockValue = redisTemplate.opsForValue().get("lock");
                // get和delete原子操作
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                redisTemplate.execute(
                        new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型
                        Arrays.asList("lock"), // 参数
                        lockValue); // 参数值，锁的值
            }
            return catalogDataFromDb;
        }else {
            /*
            占锁失败 自旋等待锁
            为避免高频率重试，此处可设置线程休眠一段时间
             */
            try {
                // 睡眠0.1s后，重新调用
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //自旋
            System.out.println(">>>>>>>>>>>>>>>>>占锁失败 自旋等待锁>>>>>>>>>>>");
            return getCatalogJsonFromDbwithRedisLock();
        }

    }

    /**
     * 本地锁的方式从数据库查询数据
     * @return
     */
    public  Map<String, List<Catelog2Vo>> getCatalogJsonFromDbwithLocalLock() {
        /*
        加锁防止缓存击穿
        this 表示当前对象，spring boot 中 所有的组件在容器中都是单例 synchronized (this) 能够锁住当前代码块
        TODO 这种加锁方式，仅仅适用于单体应用下，分布式下不适用
         */
        synchronized (this){
            return getCatalogDataFromDb();
        }
    }

    /**
     * 从数据库 获取页面需要的三级分类JSON数据
     * @return
     */
    private Map<String, List<Catelog2Vo>> getCatalogDataFromDb() {
        /*
         去缓存里查询确认是否依然没有需要的数据
          */
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 先从缓存获取
        String catalogJson = ops.get("catalogJson");
        if(!StringUtils.isEmpty(catalogJson)){
            // 缓存中存在的话直接转换类型返回
            return JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        }
        /*
        缓存中没有，开始查询数据库
         */
        System.out.println("------------------------------------------------------------------>查询了数据库");
        // 查询出所有分类
        List<CategoryEntity> categoryEntityList = this.list();
        /*
        1 查出所有一级分类
         */
        List<CategoryEntity> categoryEntities1 = getSunCatelog(categoryEntityList,0L);
        /*
        2 封装一级分类下的2/3级分类
         */
        // // map的key:一级分类ID
        Map<String, List<Catelog2Vo>> result = categoryEntities1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // map的value：二级分类数据
            List<CategoryEntity> categoryEntities2 = getSunCatelog(categoryEntityList,v.getCatId());
            List<Catelog2Vo> catelog2Vos = null;
            if (null != categoryEntities2) {
                catelog2Vos = categoryEntities2.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getCatId().toString(), l2.getName(), null);
                    // 查找当前二级分类的三级分类
                    List<CategoryEntity> categoryEntities3 = getSunCatelog(categoryEntityList,l2.getCatId());
                    // 封装为页面需要的VO模型
                    if(null != categoryEntities3){
                        List<Catelog2Vo.Catelog3Vo> catelog3Vos = categoryEntities3.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(catelog3Vos);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        /*
        从数据库查询到，返回结果之前，将数据存储到redis
         */
        // 转为JSON格式数据
        String jsonString = JSON.toJSONString(result);
        // 向缓存存一份，设置过期时间为1天，防止缓存雪崩
        ops.set("catalogJson",jsonString,1, TimeUnit.DAYS);
        return result;
    }

    /**
     * 递归收集所有父分类
     * @param catelogId 当前分类
     * @param paths
     * @return
     */
    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        // 先添加当前分类ID
        paths.add(catelogId);
        // 当前分类对象
        CategoryEntity byId = this.getById(catelogId);
        // 当前分类父级ID不为0 说明当前分类不是最顶级，继续递归查询
        if(byId.getParentCid() !=0){
            findParentPath(byId.getParentCid(),paths);
        }
        return paths;
    }

    /**
     * 递归查询子分类
     * @param root   当前节点的父id
     * @param all    所有的商品分类
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
        List<CategoryEntity> childrenList = all.stream().filter(categoryEntity -> {
            // 如果父ID等于当前分类ID： 因为ID字段类型为long 不能直接使用双等号判断是否相等，需要使用equals方法判断值是否相等
            return categoryEntity.getParentCid().equals(root.getCatId());
        }).map(categoryEntity -> {
            //子菜单可能还有子菜单, 因此递归查询 ,   查询出子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity, all));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            //进行排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return childrenList;
    }

}